---
title: 路由
slug: routing
date: 0005/01/01
number: 5
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9517814403/
photoAuthor: Mike Lewinski
contents: 学习 Meteor 的路由。|创建拥有唯一 URL 的帖子讨论页。|学习如何正确链接到这些 URL。
paragraphs: 72
---

现在，我们已经创建了一个帖子列表页面（最终是由用户提交的），我们还需要添加一个单独的帖子页面，提供给用户评论对应的帖子。

我们希望可以通过**固定链接**访问到每个单独的帖子页面，URL 形式是 `http://myapp.com/posts/xyz`（这里的 `xyz` 是 MongoDB 的 `_id` 标识符），对于每个帖子来说是唯一的。

这意味着我们需要某些**路由**来看看浏览器的地址栏里面的路径是什么，并相应地显示正确的内容。

### 添加 Iron Router 包

[Iron Router](https://github.com/EventedMind/iron-router) 是特别为了 Meteor Apps 开发的路由包。

它不仅能帮助路由（设置路径），还能帮助过滤（为这些路径分配跳转），甚至能管理订阅（控制路径可以访问哪些数据）。（注意：Iron Router 是由本书*《Discover Meteor》*的其中一名作者 Tom Coleman 参与开发的。)

首先，让我们从 Atmosphere 中安装这个包：

~~~bash
meteor add iron:router
~~~
<%= caption "Terminal 终端" %>

这个命令是下载并安装 Iron Router 包到我们的 App，这样我们就可以使用了。请注意，在能够顺利使用这个包之前，你可能需要重启你的 Meteor 应用（通过按 `ctrl + c` 就能停止进程，然后输入 `meteor` 再次启动它）。

<% note do %>

### 路由器的词汇

在本章我们会接触很多路由器的不同功能。如果你对类似 Rails 的框架有一定实践经验的话，你可能已经很熟悉大部分的这些词汇概念了。但是如果没有的话，这里有一个快速词汇表让你来了解一下：

- **路由规则（Route）**：路由规则是路由的基本元素。它的工作就是当用户访问 App 的某个 URL 的时候，告诉 App 应该做什么，返回什么东西。
- **路径（Path）**：路径是访问 App 的 URL。它可以是静态的（`/terms_of_service`）或者动态的（`/posts/xyz`），甚至还可以包含查询参数（`/search?keyword=meteor`）。
- **目录（Segment）**：路径的一部分，使用正斜杠（`/`）进行分隔。
- **Hooks**：Hooks 是可以执行在路由之前，之后，甚至是路由正在进行的时候。一个典型的例子是，在显示一个页面之前检测用户是否拥有这个权限。
- **过滤器（Filter）**：过滤器类似于 Hooks ，为一个或者多个路由规则定义的全局过滤器。
- **路由模板（Route Template）**：每个路由规则指向的 Meteor 模板。如果你不指定，路由器将会默认去寻找一个具有相同名称的模板。
- **布局（Layout）**：你可以想象成一个数码相框的布局。它们包含所有的 HTML 代码放置在当前的模板中，即使模板发生改变它们也不会变。
- **控制器（Controller）**：有时候，你会发现很多你的模板都在重复使用一些参数。为了不重复你的代码，你可以让这些路由规则继承一个**路由控制器（Routing Controller）**去包含所有的路由逻辑。

关于更多 Iron Router 的信息，请查看  [GitHub上面的完整文档](https://github.com/EventedMind/iron-router).

<% end %>

### 路由：把 URL 映射到模板

到目前为止，我们已经使用了一些固定模板（比如 `{{> postsList}}`）来为我们布局。因此，尽管我们 App 的内容还可以更改，但是页面的基本结构都已经不变了：一个头（header），它下面是帖子列表。

Iron Router 负责处理在 HTML `<body>` 标签里面该呈现什么，让我们摆脱了这个枷锁。所以我们不会再自己去定义标签里面的内容，取而代之的是，我们将路由器指定到一个包含 `{{> yield}}` 标签的布局模板。

这个 `{{> yield}}` 标签将会定义一个动态区域，它会自动呈现对应于当前线路的相应模板（从现在起，我们将指定这个特殊的模板叫 “route templates”）：

<%= diagram "router-diagram", "布局和模板。", "pull-center" %>

我们将开始构建我们的布局和添加 `{{> yield}}` 标签。首先，我们先从 `main.html` 文件里面删除 `<body>` 标签，并把它的内容放到它们共同的模板 `layout.html` 里面（保存在新的 `client/templates/application` 文件夹中）。

我们把 `main.html` 删减内容之后应该是这样的：

~~~html
<head>
  <title>Microscope</title>
</head>
~~~
<%= caption "client/main.html" %>

而新创建的 `layout.html` 现在将会包含 App 的外层布局：

~~~html
<template name="layout">
  <div class="container">
    <header class="navbar navbar-default" role="navigation">
      <div class="navbar-header">
        <a class="navbar-brand" href="/">Microscope</a>
      </div>
    </header>
    <div id="main" class="row-fluid">
      {{> yield}}
    </div>
  </div>
</template>
~~~
<%= caption "client/templates/application/layout.html" %>

你会注意到我们已经把 `yield` helper 取代了 `postsList` 模板。

完成之后，我们浏览器标签会显示 Iron Router 默认的帮助页面。这是因为我们还没有告诉路由怎样处理 `/` URL，所以它仅仅呈现一个空的模板。

接下来，我们可以恢复之前的根路径 `/` URL 映射到 `postsList` 模板。然后我们在根目录创建一个 `/lib` 目录，并在里面创建 `router.js` 文件：

~~~js
Router.configure({
  layoutTemplate: 'layout'
});

Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js"%>

我们已经完成了两件重要的事情。第一，我们已经告诉路由器使用我们刚刚创建的 `layout` 模板作为所有路由的默认布局。

第二，我们已经定义了一个名为 `postsList` 的路由规则，并映射到 `/` 路径。

<% note do %>

### `/lib` 文件夹

你放在 `/lib` 文件夹里面的所有文件都会在你的 App 运行的时候确保首先被加载（可能除了 smart 包）。这是放置需要随时准备使用的辅助代码的好地方。

不过有一点注意的是：因为 `/lib` 文件夹并不是放在 `/client` 或 `/server` 文件夹里面，这意味着它的代码将会同时存在于客户端和服务器。

<% end %>

### 路由规则的名字

在这里我们先清除一些歧义。我们有一个路由规则，叫做叫 `postsList` ，同时我们也有一个名字叫 `postsList` 的**模板**。这里是怎么回事？

默认情况下，Iron Router 会为这个路由规则，指定相同名字的模板。而如果路径（`path` 参数）没有指定，它也会根据路由规则的名字，去指定同样名字的**路径**。举个例子，在上面的设置中，如果我们不提供 `path` 参数，那么访问 `/postsList` 将会自动获取到 `postList` 模板。

你可能想知道为什么我们需要在一开始去制定路由规则。这是因为 Iron Router 的部分功能需要使用路由规则去生成 App 的链接信息。其中最常见的一个是 `{{pathFor}}` 的 Spacebars helper，它需要返回路由规则的 URL 路径。

我们希望主页链接到帖子列表页面，所以除了指定静态的 `/` URL ，我们还可以使用 Spacebars helper。虽然它们的效果是一样的，不过这给了我们更多的灵活性，如果我们更改了路由规则的映射路径，helper 仍然可以输出正确的 URL 。

~~~html
<header class="navbar navbar-default" role="navigation">
  <div class="navbar-header">
    <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
  </div>
</header>

//...
~~~
<%= caption "client/templates/application/layout.html"%>
<%= highlight "3" %>

<%= commit "5-1", "非常基本的路由。" %>

### 等待数据

如果你要部署当前版本的 App（或启动起来去使用上面的链接），你会注意到在所有帖子完全出现之前，列表里面会空了一段时间。这是因为在第一次加载页面的时候，要等到 `posts` 订阅完成后，即从服务器抓取完帖子的数据，才能有帖子显示在页面上。

这应该要有一个更好的用户体验，比如提供一些视觉上的反馈让用户知道正在读取数据，这样用户才会去继续等待。

幸好 Iron Router 给了我们一个简单的方法去实现它。我们把订阅放到 `waitOn` 的返回上。

我们把 `posts` 订阅从 `main.js` 移到路由文件中：

~~~js
Router.configure({
  layoutTemplate: 'layout',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3" %>

我们这里所谈论的是对于网站的*每个*路由（我们现在只有一个，但是我们马上会添加更多！）我们都订阅了 `posts` 订阅。

这和我们之前做的（订阅原来被放在了 `main.js` 文件中，这文件现在应该是空的了，可以删除）关键区别在于 Iron Router 现在可以得知路由什么时候准备好——即当路由得到它需要渲染的数据时。

### Get A Load Of This

如果我们只是显示一个空的模板的话，得知 `postsList` 路由已准备好也做不了什么事情。幸好 Iron Router 自带了一个延缓显示模板的方法，在路由调用模板准备好前，显示一个 `loding` 加载模板：

~~~js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4" %>

注意，因为我们在路由器级别上全局定义了 `waitOn` 方法，所以这个只会在用户第一次访问你的 App 的时候发生一次。在那之后，数据已经被加载到了浏览器的内存，路由器不需要再次去等待它。

最后一块拼图是加载模板。我们将会使用 `spin` 包去创建一个帅气的动画加载画面。通过 `meteor add sacha:spin` 去添加它，然后在 `client/templates/includes` 文件夹内创建 `loading` 模板：

~~~html
<template name="loading">
  {{>spinner}}
</template>
~~~
<%= caption "client/templates/includes/loading.html" %>

注意 `{{> spinner}}` 是 `spin` 包中的一个模板标签。尽管这部分是来自我们的 App 之外，不过我们就像其他模板一样去使用它就可以了。

这是一个好办法去等待你的订阅，不仅为了用户体验，还因为它可以顺利地确保数据可以马上体现在模板上。这消除了需要处理的模板被呈现之前，底层数据必须可用的问题，这往往需要复杂的解决方案。

<%= commit "5-2", "等待帖子的订阅。" %>

<% note do %>

### 第一次接触响应性

响应性是 Meteor 的一个核心部分，虽然我们没有真正的接触到，但我们的加载模板给了我们去接触这个概念的机会。

如果数据还没有加载完成的时候重定向去一个加载模板是很好，不过路由器如何知道在什么时候数据加载完，然后用户应该要重定向回到原本的页面呢？

刚刚我们说的这个就是响应性的体现，不过别担心，很快你会了解到关于它的更多东西。

<% end %>

### 路由到一个特定的帖子

既然我们已经看到了如何路由到 `postsList` 模板上，现在让我们建立一个路由来显示一个帖子的详细信息吧。

这里有一个问题：我们不能继续单独定义路由规则与路径的映射，因为可能有成千上万个。所以我们需要建立一个**动态**的路由规则，并让路由规则去显示我们要查看的帖子。

首先，我们将创建一个新的模板，简单地呈现相同的我们使用在帖子列表的模板。

~~~html
<template name="postPage">
  {{> postItem}}
</template>
~~~
<%= caption "client/templates/posts/post_page.html" %>

我们以后还会添加更多的元素在这个模板上（如注释），但现在它将仅仅作为放置 `{{> postItem}}` 的外壳。

我们准备创建另一个路由规则，这次 URL 路径 `/posts/<ID>` 映射到 `postPage` 模板:

~~~js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
  name: 'postPage'
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "8~10" %>

这个特殊的 `:_id` 标记告诉路由器两件事：第一，去匹配任何符合 `/posts/xyz/`（“xyz”可以是任意字符）格式的路线。第二，无论“xyz”里面是什么，都会把它放到路由器的 `params` 数组中的 `_id` 属性里面去。

请注意，我们这里只使用 `_id` 只是为了方便起见。路由器是没有办法知道你是通过一个实际的 `_id` ，还是仅仅通过一些随机的字符去访问。

我们现在路由到正确的模板了，但是我们仍然漏了一个事情：路由器通过这个帖子的 `_id` 可以知道我们想显示哪个帖子，但模板还没有线索。那么，我们要如果解决这个问题呢？

值得庆幸的是，路由器有一个聪明的内置解决方案：它允许你指定一个**数据源**。你可以把数据源想象成填充的一个美味的蛋糕去填充模板和布局。简单的说，就是你的模板要填上：

<%= diagram "router-diagram-2", "The data context.", "pull-center" %>

在我们的例子中，我们可以从 URL 上获取 `_id` ，并通过它找到我们的帖子从而获得正确的数据源：

~~~js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
  name: 'postPage',
  data: function() { return Posts.findOne(this.params._id); }
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "10" %>

所以每次用户访问这条路由规则，我们会找到合适的帖子并将其传递给模板。记住，`findOne` 返回的是一个与查询相匹配的帖子，而仅仅需要提供一个 `id` 作为参数，它可以简写成 `{_id: id}` 。

在路由规则的 `data` 方法里面，`this` 对应于当前匹配的路由对象，我们可以使用 `this.params` 去访问一个比配项（在 `path` 中通过 `:` 前缀去表示它们）。

<% note do %>

### 更多关于数据源

通过设置模板的**数据源**，你可以在模板 helper 里面控制 `this` 的值。

这个工作通常会隐式地被 `{{#each}}` 迭代器完成，它会自动设置对应的数据源到每个正在迭代的当前项中：

~~~html
{{#each widgets}}
  {{> widgetItem}}
{{/each}}
~~~

当然我们也可以使用 {{#with}} 去显式地操作，它就像简单地说“拿这个对象，提供给下面的模板应用”。例如，我们可以这样写：

~~~html
{{#with myWidget}}
  {{> widgetPage}}
{{/with}}
~~~

因此通过传递数据源作为**参数**给模板调用也可以实现相同的效果，所以前面的代码块可以重写为：

~~~js
{{> widgetPage myWidget}}
~~~

想深入了解数据源，建议[阅读我们的博客帖子](https://www.discovermeteor.com/blog/a-guide-to-meteor-templates-data-contexts/)。

<% end %>

### 使用动态的路由 Helper

最后，我们 要创建一个新的“评论”按钮，并指向正确的帖子页面。我们可以做一些像 `<a href="/posts/{{_id}}">` 这种动态模式，不过使用路由 Helper 会更可靠一点。

我们已经把帖子路由规则命名为 `postPage` ，所以我们可以使用 `{{pathFor 'postPage'}}` helper ：

~~~html
<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
  </div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "5-3", "路由到一个单独的帖子页面。" %>

不过等等，路由器到底如何准确地知道从 `/posts/xyz` 中的哪个位置去获得 `xyz` 路径？毕竟，我们没有传递任何的 `_id` 给它。

事实证明，Iron Router 是足够聪明地自己去发现它。我们告诉路由器使用 `postPage` 路由规则，而路由器知道这条规则的某些地方需要使用 `_id`（因为这是我们定义 `path` 的办法）。

因此，路由器将会在 `{{pathFor 'postPage'}}` 的上下文环境（即 `this` 对象）中寻找这个 `_id`。而在这个例子中，`this` 对象对应着一个帖子，它就是我们要寻找的拥有 `_id` 属性的地方。

又或者，你可以通过传递 Helper 的第二个参数，来明确指定需要找的 `_id` 在哪里。例如，`{{pathFor 'postPage' someOtherPost}}`。实际情况下，如果要获取帖子列表中前一个或者后一个的链接，我们就会使用这种模式。

为了看看它是否已经正常运作，我们去浏览帖子列表页面并点击其中一个“Discuss”的链接。你应该看到类似这样的：

<%= screenshot "5-2", "一个单独的帖子页面。" %>

<% note do %>

### HTML5 pushState

这里我们需要知道的是，这些 URL 变化的产生原因是正在使用 [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history?redirectlocale=en-US&redirectslug=Web%2FGuide%2FDOM%2FManipulating_the_browser_history).

路由器通过处理 URLs 的点击去访问网站的内部，这样可以防止浏览器跳出我们的 App ，而不只是为了必要的改变 App 的状态。

如果一切运作正常的话，页面应该会瞬间改变。事实上，有时候事情变化得过快，可能需要某种类型的过渡页面。这是本章的范围之外的，但却是一个有趣的话题。

<% end %>

### 帖子无法找到

让我们别忘了路由工作两种方式：改变我们访问的页面 URL，也能显示我们改变 *URL* 的新页面。所以我们需要解决当某用户输入*错误的* URL 时的情况。

幸好，Iron Rounter 可以通过 `notFoundTemplate` 选项来为我们解决这个问题。

首先，我们设置一个新模板来显示简单的 404 错误 信息：

~~~html
<template name="notFound">
  <div class="not-found jumbotron">
    <h2>404</h2>
    <p>Sorry, we couldn't find a page at this address. 抱歉，我们无法找到该页面。</p>
  </div>
</template>
~~~
<%= caption "client/templates/application/not_found.html"%>

然后，我们将 Iron Rounter 指向这个模板：

~~~js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  notFoundTemplate: 'notFound',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

//...
 ~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>

为了验证这个错误页面，你可以尝试随机输入 URL 像 `http://localhost:3000/nothing-here`。

但是稍等，如果有人输入了像 `http://localhost:3000/posts/xyz` 这种格式的 URL，`xyz` *不是*一个合法的帖子 `_id` 怎么办？虽然是合法的路由，但是没有指向任何数据。

幸好，如果我们在 `route.js` 结尾添加了特别的 `dataNotFound` hook，Iron Rounter 就能足够智能地解决这个问题。

~~~js
//...

Router.onBeforeAction('dataNotFound', {only: 'postPage'});
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>

这会告诉 Iron Router 不仅在非法路由情况下，而且在 `postPage` 路由，每当 `data` 函数返回“falsy”（比如 `null`、`false`、`undefined` 或 空）对象时，显示“无法找到”的页面。

<%= commit "5-4", "添加了页面无法找到的模板。" %>

<% note do %>

### 为什么叫 “Iron”?

你也许会想知道命名“Iron Router”背后的故事。根据 Iron Router 的作者 Chris Mather，因为流星（meteor）主要由铁（iron）元素构成的事实。

<% end %>
